/*
 * Copyright (C) 2023 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.android.tools.idea.logcat.service

import com.android.logcat.proto.LogcatEntryProto
import com.android.logcat.proto.LogcatPriorityProto
import com.android.processmonitor.monitor.testing.FakeProcessNameMonitor
import com.android.testutils.TestResources
import com.android.tools.idea.logcat.message.LogLevel.DEBUG
import com.android.tools.idea.logcat.message.LogLevel.ERROR
import com.android.tools.idea.logcat.message.LogLevel.INFO
import com.android.tools.idea.logcat.message.LogLevel.VERBOSE
import com.android.tools.idea.logcat.message.LogLevel.WARN
import com.android.tools.idea.logcat.message.LogcatMessage
import com.android.tools.idea.logcat.util.logcatMessage
import com.google.common.truth.Truth.assertThat
import com.google.protobuf.ByteString
import java.nio.ByteBuffer
import java.nio.ByteOrder.LITTLE_ENDIAN
import java.time.Instant
import java.time.temporal.ChronoField
import java.util.concurrent.TimeUnit
import kotlin.random.Random
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.runBlocking
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TestName

/** Tests for [LogcatProtoShellCollector] */
class LogcatProtoShellCollectorTest {
  @get:Rule val testNameRule = TestName()

  private val random = Random(System.currentTimeMillis())
  private val fakeProcessNameMonitor = FakeProcessNameMonitor()

  /**
   * This tests verifies the correct content of logs.
   *
   * The log file was pulled from an actual device and corresponds to:
   * ```
   * 02-09 12:25:23.650  2048  2048 V FooBar  : Verbose message
   * 02-09 12:25:23.650  2048  2048 D FooBar  : Debug message
   * 02-09 12:25:23.650  2048  2048 I FooBar  : Info message
   * 02-09 12:25:23.650  2048  2048 W FooBar  : Warning message
   * 02-09 12:25:23.650  2048  2048 E FooBar  : Error message
   * 02-09 12:25:23.650  2048  2048 F FooBar  : WTF message
   * ```
   *
   * Note that in SDK 16, `Log.wtf()` logs an ASSERT message with a newline appended to it.
   */
  @Test
  fun testContent() {
    val processName = "com.alonalbert.myapplication"
    val appId = "app-id"
    val pid = 6153
    val tag = "FooBar"
    fakeProcessNameMonitor.addProcessName("serial", pid, appId, "foo")
    val collector = LogcatProtoShellCollector("serial", fakeProcessNameMonitor)

    val messages = collector.loadMessageFromFile("/logcatFiles/logcat-small.proto")

    val timestamp = Instant.ofEpochSecond(1697744872, TimeUnit.MILLISECONDS.toNanos(489))
    assertThat(messages)
      .containsExactly(
        logcatMessage(VERBOSE, pid, pid, appId, processName, tag, timestamp, "Verbose msg"),
        logcatMessage(DEBUG, pid, pid, appId, processName, tag, timestamp, "Debug msg"),
        logcatMessage(INFO, pid, pid, appId, processName, tag, timestamp, "Info msg"),
        logcatMessage(WARN, pid, pid, appId, processName, tag, timestamp, "Warning msg"),
        logcatMessage(ERROR, pid, pid, appId, processName, tag, timestamp, "Error msg"),
        logcatMessage(ERROR, pid, pid, appId, processName, tag, timestamp, "WTF msg"),
      )
      .inOrder()
  }

  /*
   * The following test uses a random buffer size when reading actual logs generated by a device.
   *
   * We use a random buffer size in order to put more pressure on the algorithm. When a test fails, we log the buffer size used, so it can
   *  be reproduced later.
   */
  @Test
  fun randomBufferSize() {
    // Test file created with `adb shell logcat -d -B -t 4000`
    testFromFile("/logcatFiles/logcat-4000.proto", expectedCount = 4000)
  }

  @Test
  fun realisticBufferSize() {
    // Test file created with `adb shell logcat -d -B -t 4000`
    testFromFile("/logcatFiles/logcat-4000.proto", expectedCount = 4000, bufferSize = 8192)
  }

  @Test
  fun tinyBufferSize() {
    // Test file created with `adb shell logcat -d -B -t 100`
    testFromFile("/logcatFiles/logcat-100.proto", expectedCount = 100, bufferSize = 1)
  }

  @Test
  fun emptyProcessName() {
    val processName = "com.alonalbert.myapplication"
    val appId = "app-id"
    val pid = 6153L
    fakeProcessNameMonitor.addProcessName("serial", pid.toInt(), appId, processName)
    val collector = LogcatProtoShellCollector("serial", fakeProcessNameMonitor)
    val proto =
      LogcatEntryProto.newBuilder()
        .setPriority(LogcatPriorityProto.INFO)
        .setPid(pid)
        .setTid(pid)
        .setNullTerminatedTag("tag")
        .setMessage(ByteString.copyFromUtf8("message"))
        .build()

    val messages = collector.loadMessageFromBuffer(proto.toByteBuffer())

    assertThat(messages)
      .containsExactly(
        logcatMessage(
          timestamp = Instant.EPOCH,
          pid = pid.toInt(),
          tid = pid.toInt(),
          logLevel = INFO,
          tag = "tag",
          appId = appId,
          processName = processName,
          message = "message",
        )
      )
  }

  private fun testFromFile(
    filename: String,
    expectedCount: Int,
    bufferSize: Int = random.nextInt(10, 1000),
  ): Unit = runBlocking {
    val bytes = TestResources.getFile(filename).readBytes()
    val buffers = bytes.asIterable().chunked(bufferSize) { ByteBuffer.wrap(it.toByteArray()) }
    val collector = LogcatProtoShellCollector("", fakeProcessNameMonitor)
    val actualCount = flow { buffers.forEach { collector.collectStdout(this, it) } }.countMessages()

    val assertionText = "Failed with bufferSize=$bufferSize"
    assertThat(actualCount).named(assertionText).isEqualTo(expectedCount)
  }
}

private suspend fun Flow<List<LogcatMessage>>.countMessages(): Int {
  var count = 0
  collect { count += it.size }

  return count
}

private fun LogcatProtoShellCollector.loadMessageFromBuffer(
  buffer: ByteBuffer
): List<LogcatMessage> = runBlocking {
  val messages = flow { collectStdout(this, buffer) }.toList().flatten()
  messages.map {
    val timestamp = it.header.timestamp
    val millis = timestamp.getLong(ChronoField.MILLI_OF_SECOND)
    val roundedTimestamp =
      Instant.ofEpochSecond(timestamp.epochSecond, TimeUnit.MILLISECONDS.toNanos(millis))
    it.copy(it.header.copy(timestamp = roundedTimestamp))
  }
}

private fun LogcatProtoShellCollector.loadMessageFromFile(filename: String): List<LogcatMessage> =
  loadMessageFromBuffer(ByteBuffer.wrap(TestResources.getFile(filename).readBytes()))

private fun LogcatEntryProto.toByteBuffer(): ByteBuffer {
  val bytes = toByteArray()
  return ByteBuffer.allocate(Long.SIZE_BYTES + bytes.size)
    .order(LITTLE_ENDIAN)
    .putLong(bytes.size.toLong())
    .put(bytes)
    .rewind()
}

private fun LogcatEntryProto.Builder.setNullTerminatedTag(tag: String): LogcatEntryProto.Builder {
  val output = ByteString.newOutput()
  output.write(tag.toByteArray())
  output.write(0)
  setTag(output.toByteString())
  return this
}
